Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/santiagodc8/tu_perfil.net/llms.txt

Use this file to discover all available pages before exploring further.

The home page at / is the main entry point for readers. It is a Next.js server component that fetches all data from Supabase at build time and revalidates every 60 seconds using Incremental Static Regeneration (ISR).
src/app/(public)/page.tsx
export const revalidate = 60; // Revalidate every 60 seconds

Data flow

Each request cycle follows this path:
1

Supabase queries run on the server

The page component runs five parallel (or sequential) Supabase queries: featured articles for the carousel, all categories and their latest articles, popular articles this week, trending articles in the last 24 hours, and active ads.
2

RPCs resolve ranked lists

The popular_articles_this_week and trending_articles_24h RPCs return ranked article IDs. The page then fetches full article details for those IDs and re-sorts them to preserve the ranking order.
3

Server component renders HTML

All data is passed as props to client components (HeroCarousel, TrendingSection, SidebarPublic, AdBanner, CategorySection). The server sends fully-rendered HTML — no client-side data fetching on page load.
4

ISR cache serves subsequent requests

Vercel caches the rendered page and revalidates it in the background every 60 seconds, ensuring content stays fresh without rebuilding on every hit.
The carousel appears at the top of the page and showcases the most important articles. Data source: The page first queries for articles where featured = true, ordered by created_at descending, limited to 5.
src/app/(public)/page.tsx
const { data: featuredData } = await supabase
  .from("articles")
  .select("title, slug, excerpt, image_url, created_at, category:categories(name, color, slug)")
  .eq("published", true)
  .eq("featured", true)
  .order("created_at", { ascending: false })
  .limit(5)
  .returns<HomeArticle[]>();
Fallback: If no featured articles exist, the page falls back to the 3 most recently published articles.
src/app/(public)/page.tsx
if (heroSlides.length === 0) {
  const { data: latestData } = await supabase
    .from("articles")
    .select("title, slug, excerpt, image_url, created_at, category:categories(name, color, slug)")
    .eq("published", true)
    .order("created_at", { ascending: false })
    .limit(3)
    .returns<HomeArticle[]>();

  heroSlides = latestData ?? [];
}
Component: HeroCarousel is a client component that auto-advances every 6 seconds (INTERVAL_MS = 6000), pauses on hover, and renders previous/next arrow buttons and dot indicators. A thin red progress bar animates along the bottom of each slide. Each slide is a full-bleed image with a gradient overlay at the bottom. It displays the category badge, article title, excerpt (hidden on mobile), and publication date. The entire slide is a <Link> pointing to /noticia/{slug}. Below the carousel, a “Tendencias del día” (Trending today) strip shows up to 5 articles that received the most page views in the last 24 hours. Data source: The trending_articles_24h RPC returns ranked article IDs. The page fetches full details and re-sorts by rank.
src/app/(public)/page.tsx
const { data: trendingIds } = await supabase.rpc("trending_articles_24h", {
  lim: 5,
});
The TrendingSection component renders a 5-column grid (stacked on mobile). Each card shows a thumbnail, a numbered rank badge, the category name in the category color, and the article title.

Category sections

Below trending, all categories are rendered in a 2/3 + 1/3 grid layout.
src/app/(public)/page.tsx
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 sm:gap-8">
  {/* Main content — 2/3 width */}
  <div className="lg:col-span-2 space-y-8 sm:space-y-10">
    {categoryArticles.map((cat, index) => (
      <div key={cat.id}>
        <CategorySection
          name={cat.name}
          slug={cat.slug}
          color={cat.color}
          articles={cat.articles}
        />
        {/* Between-articles ad after each category */}
        {betweenAds[index] && (
          <div className="mt-8 sm:mt-10">
            <AdBanner ad={betweenAds[index]} />
          </div>
        )}
      </div>
    ))}
  </div>

  {/* Sidebar — 1/3 width, sticky */}
  <div className="lg:col-span-1">
    <div className="lg:sticky lg:top-16">
      <SidebarPublic popular={popular} sidebarAds={sidebarAds} />
    </div>
  </div>
</div>
Each CategorySection shows the category name (with its color accent bar), a “Ver más” link to the full category page (/{slug}), and up to 3 recent articles from that category. The sticky right sidebar (SidebarPublic) contains three elements in order:
  1. Search box — a <form> that submits to /buscar?q={query}.
  2. First sidebar ad — rendered via AdBanner if a sidebar-position ad is active.
  3. Most read this week — a ranked list of up to 5 articles from popular_articles_this_week. Each entry shows a large rank number alongside a small article card.
  4. Additional sidebar ads — any remaining sidebar-position ads rendered after the popular list.
Data source for popular articles:
src/app/(public)/page.tsx
const { data: popularIds } = await supabase.rpc("popular_articles_this_week", {
  lim: 5,
});
Fallback: If the RPC returns no results (e.g., no page views recorded yet), the sidebar falls back to the 5 articles with the highest total views count.

Ad placements

Ads are fetched from the ads table filtered by active = true and sorted by sort_order.
src/app/(public)/page.tsx
const headerAds    = allAds.filter((a) => a.position === "header");
const sidebarAds   = allAds.filter((a) => a.position === "sidebar");
const betweenAds   = allAds.filter((a) => a.position === "between_articles");

Header banner

Rendered at the very top of the page, above the hero carousel. Supports multiple header ads stacked vertically.

Sidebar ads

Rendered inside SidebarPublic. The first ad appears above the popular list; additional ads appear below it.

Between-article ads

One between_articles ad is injected after each category section in the main column, indexed by category position.
Each AdBanner renders a clickable image that links to link_url. Clicking opens the link in a new tab.
If no ads are configured or all ads are inactive, the ad slots are simply hidden — no empty space is left in the layout.